﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using DG.Tweening;
using FibMatrix.DS;
using UnityEngine;
using Object = UnityEngine.Object;

public class NormalRendererColorAdjustor : MonoBehaviour 
{
	class SharedMaterialInfo
	{
		public SharedMaterialInfo(Material sourceMat, int rgba, bool immutable)
		{
			this.sourceMat = sourceMat;
			this.smId = sourceMat.GetInstanceID();
			this.rgba = rgba;
			this.immutable = immutable;
		}
		
		public Material mat;
		public Material sourceMat;
		public int smId;
		public int rgba;
		public int refCount;
		public bool immutable;
	}

	class MaterialInfoTable : IntHashtable<SharedMaterialInfo>
	{
		public MaterialInfoTable(int capacity):base(capacity)
		{
			
		}

		public SharedMaterialInfo opaqueSMI;
		public SharedMaterialInfo transparencySMI;
	}

	private const string DEFAULT_COLOR_NAME = "_Color";
	private const float ALPHA_CHANGE_THRESHOLD = 1f / 256;
	private const int DEFAULT_TRANS_RENDERER_QUEUE = 3000;
	private const int PER_MATERIAL_MAX_COLOR_COUNT = 10;
	private const int MPB_OPAQUE_ID = 0x000000FF;
	private const int MPB_TRANSPARENCY_ID = 0x00000000;

	// private static MaterialPropertyBlock mpb = null;
	private int colorNameId = -1;
    
	public Renderer[] renderers;
	public string colorName;
	// public int opaqueRendererQueue = DEFAULT_OPAQUE_RENDERER_QUEUE;
	public int transparentRendererQueue = DEFAULT_TRANS_RENDERER_QUEUE;
	public Shader transparentShader;

	private SharedMaterialInfo[] _materials;
	private Tweener _tweener;
	private bool _lastTransparency = false;

	private static MaterialPropertyBlock _mpb = null;
	private static IntHashtable<MaterialInfoTable> _materialRefMap = new IntHashtable<MaterialInfoTable>(20);
	private static Stack<SharedMaterialInfo> _smiPool = new Stack<SharedMaterialInfo>(4);
	
	public static void ResetAll()
	{
		_materialRefMap.Clear();
		_smiPool.Clear();
	}
	
	void Awake()
	{
		if (_mpb == null)
		{
			_mpb = new MaterialPropertyBlock();
		}
		this.enabled = false;
		if (string.IsNullOrEmpty(colorName))
		{
			colorName = DEFAULT_COLOR_NAME;
		}
		SetColorParamName(colorName);
	}

	private Color _color = Color.white;

	public Color GetColor()
	{
		return _color;
	}

	public void SetColor(Color c)
	{
		if (DoSetColor(ref c))
		{
			CheckTween();
		}
	}

	public void SetColorAlpha(float alphaTo)
	{
		//特殊处理，如果alpha=0 就把Renderers直接隐藏
		if (alphaTo == 0)
		{
			if (renderers != null)
			{
				foreach (var item in renderers)
					item.enabled = false;
			}
		}
		else if (alphaTo > 0f && _color.a < ALPHA_CHANGE_THRESHOLD)
		{
			if (renderers != null)
			{
				foreach (var item in renderers)
					item.enabled = true;
			}
		}
		
		if (Math.Abs(alphaTo - _color.a) >= ALPHA_CHANGE_THRESHOLD)
		{
			CheckTween();
			_color.a = alphaTo;
			this.enabled = true;
		}
	}

    public void SetColorParamName(string name)
    {
        colorNameId = Shader.PropertyToID(name);
    }
    
    private void TweenSetColor(Color c)
    {
	    DoSetColor(ref c);
    }

    private bool DoSetColor(ref Color c)
    {
	    if (_color != c)
	    {
		    _color = c;
		    this.enabled = true;
		    return true;
	    }

	    return false;
    }

    private void CheckTween()
    {
	    if (_tweener != null)
	    {
		    if (!_tweener.active || !object.ReferenceEquals(_tweener.target, this) || _tweener.IsComplete())
		    {
			    _tweener = null;
		    }
	    }
    }

    private static SharedMaterialInfo CreateSharedMaterialInfo(Material sourceMat, int rgba, bool immutable)
    {
	    SharedMaterialInfo smi;
	    if (_smiPool.Count > 0)
	    {
		    smi = _smiPool.Pop();
		    smi.sourceMat = sourceMat;
		    smi.smId = sourceMat.GetInstanceID();
		    smi.rgba = rgba;
		    smi.immutable = immutable;
		    smi.mat.CopyPropertiesFromMaterial(sourceMat);
	    }
	    else
	    {
		    smi = new SharedMaterialInfo(sourceMat, rgba, immutable);
		    smi.mat = new Material(sourceMat);
	    }

	    return smi;
    }

    private SharedMaterialInfo GetSharedMaterialInfo(Material sourceMaterial, int rgba, bool transparency, bool forceUseCommonMat)
    {
	    int smid = sourceMaterial.GetInstanceID();
	    if (!_materialRefMap.TryGetValue(smid, out var mit))
	    {
		    _materialRefMap[smid] = mit = new MaterialInfoTable(PER_MATERIAL_MAX_COLOR_COUNT);
	    }

	    SharedMaterialInfo smi = null;
	    Material mat;
	    if (forceUseCommonMat || mit.Size >= PER_MATERIAL_MAX_COLOR_COUNT)
	    {
		    if (transparency)
		    {
			    if (mit.transparencySMI == null)
			    {
				    smi = mit.transparencySMI = CreateSharedMaterialInfo(sourceMaterial, rgba, false);
				    mat = smi.mat;
				    if (transparentShader != null)
				    {
					    mat.shader = transparentShader;
				    }
				    mat.renderQueue = transparentRendererQueue;
			    }
			    else
			    {
				    smi = mit.transparencySMI;
			    }
		    }
		    else
		    {
			    if (mit.opaqueSMI == null)
			    {
				    smi = mit.opaqueSMI = CreateSharedMaterialInfo(sourceMaterial, rgba, false);
				    mat = smi.mat;
				    mat.shader = sourceMaterial.shader;
			    }
			    else
			    {
				    smi = mit.opaqueSMI;
			    }
		    }
	    }
	    else
	    {
		    if (!mit.TryGetValue(rgba, out smi))
		    {
			    mit[rgba] = smi = CreateSharedMaterialInfo(sourceMaterial, rgba, true);
			    mat = smi.mat;
			    mat.SetVector(colorNameId, _color);
			    if (transparency)
			    {
				    if (transparentShader != null)
				    {
					    mat.shader = transparentShader;
				    }
				    mat.renderQueue = transparentRendererQueue;
			    }
			    else
			    {
				    mat.shader = sourceMaterial.shader;
			    }
		    }
	    }
	    
	    smi.rgba = rgba;

	    ++smi.refCount;
	    
	    return smi;
    }
    
	// Update is called once per frame
	void LateUpdate () 
	{
		if (renderers == null)
		{			
			Renderer component = this.GetComponent<Renderer>();
			if (component != null)
			{
				renderers = new Renderer[1];
				renderers[0] = component;
            }             
		}

		if (renderers != null)
		{
			int rendererCount = renderers.Length;
			if (_materials == null)
			{
				_materials = new SharedMaterialInfo[rendererCount];
			}

			Color32 c32 = _color;
			int rgba = 0;
			unsafe
			{
				int* cvp = (int*) &c32;
				rgba = *cvp;
			}
			
			bool transparency = c32.a != 255;
			for (int i = 0; i < renderers.Length; i++)
			{
				Renderer render = renderers[i];
				if (render != null)
				{
					SharedMaterialInfo smi = _materials[i];
					if (_tweener != null)
					{
						int expectedRGBAId = transparency ? MPB_TRANSPARENCY_ID : MPB_OPAQUE_ID;
						if (smi != null && (smi.immutable || smi.rgba != expectedRGBAId))
						{
							render.sharedMaterial = smi.sourceMat;
							RecycleSharedMaterialInfo(smi);
							smi = null;
						}
						if (smi == null)
						{
							Material sm = render.sharedMaterial;
							_materials[i] = smi = GetSharedMaterialInfo(sm, expectedRGBAId, transparency, true);
							render.sharedMaterial = smi.mat;
						}
					}
					else
					{
						if (smi != null && (!smi.immutable || smi.rgba != rgba))
						{
							render.sharedMaterial = smi.sourceMat;
							RecycleSharedMaterialInfo(smi);
							smi = null;
						}
						if (smi == null)
						{
							Material sm = render.sharedMaterial;
							_materials[i] = smi = GetSharedMaterialInfo(sm, rgba, transparency, false);
							render.sharedMaterial = smi.mat;
						}
					}

					if (!smi.immutable)
					{
						render.GetPropertyBlock(_mpb);
						_mpb.SetVector(colorNameId, _color);
						render.SetPropertyBlock(_mpb);
					}
					else
					{
						render.SetPropertyBlock(null);
					}
				}
			}

			_lastTransparency = transparency;
		}

		this.enabled = false;
	}

	public Tweener DOColor(Color endValue, float duration)
	{
		if (_tweener != null)
		{
			_tweener.Kill();
			_tweener = null;
		}
		_tweener = DOTween.To(this.GetColor, this.TweenSetColor, endValue, duration).SetTarget(this);
        return _tweener;
    }

	private static void RecycleSharedMaterialInfo(SharedMaterialInfo smi)
	{
		if (smi.refCount > 0)
		{
			--smi.refCount;
		}

		if (smi.refCount == 0)
		{
			try
			{
				if (smi.immutable)
				{
					_materialRefMap[smi.smId].Remove(smi.rgba);
				}
				else
				{
					var mit = _materialRefMap[smi.smId];
					if (mit.opaqueSMI == smi)
					{
						mit.opaqueSMI = null;
					}
					else
					{
						mit.transparencySMI = null;
					}
				}

				smi.sourceMat = null;
				smi.mat.shader = null;
				_smiPool.Push(smi);
			}
			catch (Exception e)
			{
				Debug.LogException(e);
			}
		}
	}

	private void OnDestroy()
	{
		int matCount = _materials?.Length ?? 0;
		for (int i = 0; i < matCount; ++i)
		{
			SharedMaterialInfo smi = _materials[i];
			if (smi != null)
			{
				RecycleSharedMaterialInfo(smi);
				_materials[i] = null;
			}
		}
	}
}
